#!/usr/bin/env python
#_*_ coding: latin-1 _*_

# Copyright 2011 - 2012 David Selby dave6502@gmail.com

# This file is part of FX Wizard.

# FX Wizard is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# FX Wizard is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with FX Wizard.  If not, see <http://www.gnu.org/licenses/>.

from operator import itemgetter
from wx.lib.pubsub import Publisher
from wx.lib.wordwrap import wordwrap
from PIL import ImageGrab
from exception import WizardExcept

import wx, os, sys, re, pickle, shutil, thread, traceback, pythoncom
import datetime as dt
import wx.calendar as wxcalendar
import wx.lib.masked as wxmasked
import wx.html as wxhtml
import wx.richtext as wxrtext
import wx.grid as wxgrid
import core.ocr as ocr
import time
import engine
import my_mobiler
import log

WHITE         = '#FFFFFF'
WINDOW_NORM   = '#ECE9D8'
WINDOW_EMP    = '#D6BDA7'

BGROUND       = '#C1C1C1'

STATUS_NORM   = '#0000FF'
STATUS_ALT    = '#C1C1C1'
STATUS_EMP    = '#FF0000'

DATE_ROW      = '#F6FF6D'
CLEAR_ROW     = '#B9FFAB'

DATE_ROW_ALT  = '#B6B6B6'
CLEAR_ROW_ALT = '#D5D5D5'

# REF ...

# 00 Burglary Equip Damaged
# 01 Burglary Equip not Damaged
# 02 Panic Button Operated by ...
# 03 Line Fault
# 04 Subscriber Error
# 05 Storm/Electrical Transit
# 06 Prolonged Power Failure
# 07 Tamper
# 08 Passive Infra Red Detector
# 09 Dual PIR / Microwave Det ...
# 10 Other Electronic Device
# 11 Non Electronic Device
# 12 Control/Signalling Equipment
# 13 Repair Damage
# 14 Change Customer Code
# 15 Re-Instruct Customer
# 16 Additional Work
# 17 Completion Certificate
# 18 Call Out no Access
# 19 Routine Maint Completed
# 20 Routine Maint no Access
# 21 Maint 2nd Written Attempt
# 22 Equipment Fault
# 37 Power Failure
# 38 Telephone \ Assistance


class MainWindow(wx.Frame):

    def __init__(self, parent, fx_wizard_dir, my_mobiler_dir):

        wx.Frame.__init__(self, parent, title='FX Wizard', size=wx.DisplaySize(),
        style=wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.RESIZE_BOX | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))

        self.fx_wizard_dir  = fx_wizard_dir
        self.my_mobiler_dir = my_mobiler_dir

        self.log = log.Log()
        self.dbase = []
        self.__loadRecords()

        icon = wx.Icon(os.path.join(self.fx_wizard_dir, 'data', 'wizard.ico'), wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)

        Publisher().subscribe(self.modifyRecord, 'modify_record')
        Publisher().subscribe(self.deleteRecord, 'delete_record')

        self.menu_bar = self.__setupMenuBar()
        self.tool_bar = self.__setupToolBar()
        self.grid     = self.__setupGrid(self)
        self.grid.SetDefaultCellBackgroundColour(BGROUND)
        self.CreateStatusBar()

        self.__updateGrid(self.grid, True)

        self.Show(True)
        self.audible = False

        self.status    = 0
        self.mymobiler = 0

        # nasty hack to stop selection of date rows!
        self.timer = wx.Timer(self)
        self.timer.Start(200)
        self.Bind(wx.EVT_TIMER, self.__daemonGrid, self.timer)

        HelpWindow(self, self.fx_wizard_dir)


    def __setupMenuBar(self):

        """
        Sets up the menu bar icons and bindings, all pretty straight forward
        """

        menu_file    = wx.Menu()
        menu_wizard  = menu_file.Append(wx.ID_ANY, '&Perform Clears', 
        'Link to PDA and start clearing')
        menu_file.AppendSeparator()
        menu_quit    = menu_file.Append(wx.ID_EXIT, '&Quit', 'Quit')

        menu_dbase   = wx.Menu()
        menu_add     = menu_dbase.Append(wx.ID_ANY, 
        '&Add clear', 'Add a clear or clears')
        menu_delete  = menu_dbase.Append(wx.ID_ANY, 
        '&Delete selected', 'Delete all selected clears')
        menu_dbase.AppendSeparator()
        menu_reset   = menu_dbase.Append(wx.ID_ANY, 
        '&Delete ALL', 'Delete ALL clears')

        menu_diag    = wx.Menu()
        menu__diag   = menu_diag.Append(wx.ID_ANY, 
        '&Generate Diagnostic', 'Generate diagnostic file to email to the developer')

        menu_about   = wx.Menu()
        menu__about  = menu_about.Append(wx.ID_ABOUT, 
        '&About FX Wizard', 'About FX Wizard')

        menu_help    = wx.Menu()
        menu__help   = menu_help.Append(wx.ID_ANY, 
        '&Help Using FX Wizard', 'Help Using FX Wizard')

        menu_bar = wx.MenuBar()
        menu_bar.Append(menu_file,  '&File')
        menu_bar.Append(menu_dbase, '&Database')
        menu_bar.Append(menu_diag,  '&Diagnostic')
        menu_bar.Append(menu_about, '&About')
        menu_bar.Append(menu_help,  '&Help')
        self.SetMenuBar(menu_bar)

        self.Bind(wx.EVT_MENU, self.__onWizard, menu_wizard)
        self.Bind(wx.EVT_MENU, self.__onQuit, menu_quit)
        self.Bind(wx.EVT_MENU, self.__onAddNewClear, menu_add)
        self.Bind(wx.EVT_MENU, self.__onDelSelectedClears, menu_delete)
        self.Bind(wx.EVT_MENU, self.__onDelAllClears, menu_reset)
        self.Bind(wx.EVT_MENU, self.__onBugDiagSave, menu__diag)
        self.Bind(wx.EVT_MENU, self.__onAbout, menu__about)
        self.Bind(wx.EVT_MENU, self.__onHelp, menu__help)

        return menu_bar


    def __setupToolBar(self):

        """
        Sets up the toolbar icons and bindings, unfortunate bug in where adding 
        a style parameter to 'self.CreateToolBar()' kills the '|' separator widget. 
        leaving in commented code in case I need to revert later on.
        """

        tool_bar = self.CreateToolBar()
        # tool_bar = self.CreateToolBar(wx.TB_TEXT)
        # tool_bar.SetBackgroundColour(DATE_ROW)

        # NOTE using spaces to force separation of all toolbar items, neat bodge 
        # if 'wx.TB_TEXT' is used
        tool_wizard = tool_bar.AddLabelTool(0, 'Wizard ', 
        wx.Bitmap(os.path.join(self.fx_wizard_dir, 'data', 'wizard.png')), 
        shortHelp = 'Link to the PDA and start clearing', 
        longHelp = 'Link to the PDA and start clearing')

        tool_bar.AddSeparator()
        tool_add = tool_bar.AddLabelTool(1, 'Add', 
        wx.Bitmap(os.path.join(self.fx_wizard_dir, 'data', 'add.png')), 
        shortHelp = 'Add a clear or clears', 
        longHelp = 'Add a clear or clears')
        
        tool_del = tool_bar.AddLabelTool(2, 'Delete', 
        wx.Bitmap(os.path.join(self.fx_wizard_dir, 'data', 'delete.png')), 
        shortHelp = 'Delete all the selected clears', 
        longHelp = 'Delete all the selected clears')
        
        # tool_bar.AddSeparator()
        # tool_bug = tool_bar.AddLabelTool(3, 'Generate a diagnostic', 
        # wx.Bitmap(os.path.join(self.fx_wizard_dir, 'data', 'bug.png')), 
        # shortHelp = 'Generate a diagnostic file to email to the developer', 
        # longHelp = 'Generate a diagnostic file to email to the developer')

        tool_bar.AddSeparator()
        tool_help = tool_bar.AddLabelTool(4, 'Help', 
        wx.Bitmap(os.path.join(self.fx_wizard_dir, 'data', 'help.png')), 
        shortHelp = 'Help using FX Wizard', 
        longHelp = 'Help using FX Wizard')

        tool_bar.AddSeparator()
        tool_quit = tool_bar.AddLabelTool(5, 'Quit', 
        wx.Bitmap(os.path.join(self.fx_wizard_dir, 'data', 'quit.png')), 
        shortHelp = 'Quit', 
        longHelp = 'Quit')

        tool_bar.Realize()

        # all the bindings
        self.Bind(wx.EVT_TOOL, self.__onWizard, tool_wizard)
        self.Bind(wx.EVT_TOOL, self.__onAddNewClear, tool_add)
        self.Bind(wx.EVT_TOOL, self.__onDelSelectedClears, tool_del)
        # self.Bind(wx.EVT_TOOL, self.__onBugDiagSave, tool_bug)
        self.Bind(wx.EVT_TOOL, self.__onHelp, tool_help)
        self.Bind(wx.EVT_TOOL, self.__onQuit, tool_quit)

        return tool_bar
 

    def __setupGrid(self, parent):

        """
        Sets up the grid and column titles, all nicely centered apart from
        reason and summary which are aligned left
        """

        grid = wxgrid.Grid(parent)
        grid.CreateGrid(0, 5)

        font = wx.Font(10, wx.NORMAL, wx.NORMAL, wx.LIGHT)
        grid.SetLabelFont(font)
        grid.SetDefaultCellFont(font)
        grid.SetRowLabelSize(0)
        grid.SetColLabelSize(20)
        grid.SetSelectionMode(1)
        grid.SetCellHighlightPenWidth(0)

        # lock the grid down
        grid.SetDefaultCellOverflow(False)
        grid.EnableDragColSize(False)
        grid.EnableDragRowSize(False)
        grid.EnableDragGridSize(False)
        grid.EnableEditing(False)

        grid.SetDefaultCellAlignment(wx.ALIGN_CENTER, wx.ALIGN_CENTER)
        grid.SetDefaultColSize(80)
        grid.SetDefaultRowSize(25)

        # special cases
        attr = wxgrid.GridCellAttr()
        attr.SetAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTER)

        grid.SetColSize(3, 200)
        grid.SetColAttr(3, attr)
        grid.SetColAttr(4, attr)

        grid.SetColLabelValue(0, 'Work Order')
        grid.SetColLabelValue(1, 'Start Time')
        grid.SetColLabelValue(2, 'End Time')
        grid.SetColLabelValue(3, 'Reason')
        grid.SetColLabelValue(4, 'Summary')

        self.Bind(wx.EVT_SIZE, self.__onSize)
        self.Bind(wxgrid.EVT_GRID_CELL_LEFT_DCLICK, self.__onEditSelectedClear)
        self.Bind(wxgrid.EVT_GRID_CELL_RIGHT_CLICK, self.__onEditSelectedClear)

        return grid


    def __enableMainGUI(self, flag):

        """ 
        Enables or disables the main GUI
        """

        for pos in range (5):
            self.menu_bar.EnableTop(pos, flag)

        for id_ in range(7):
            self.tool_bar.EnableTool(id_, flag)

        self.__updateGrid(self.grid, flag)

# ------------------------------------------------------------------------------


    def __updateGrid(self, parent, enabled = True):

        """
        Update the grid, a tricky bit of code. read the 'dbase', split the 
        records, sort them then carefully re-populate the grid. Does not delete
        the old grid and create a new one in case of GUI flickering
        """

        dbase_split = []

        # split the database up into a list of lists, then dual sort it
        for record in self.dbase:

            # convert date from US to UK for later sorting and display
            tmp = record['start_date'].split('/')
            date = '%s/%s/20%s' % (tmp[1], tmp[0], tmp[2])

            dbase_split.append([record['WO'], date, record['start_time'], 
            record['end_time'], record['reason'][3:], record['report']])

        dbase_split = sorted(dbase_split, key = itemgetter(0))
        dbase_split = sorted(dbase_split, key = itemgetter(1))

        current_row = 0
        old_rows    = self.grid.GetNumberRows()
        old_date    = ''

        # update the rows in the grid
        for record in dbase_split:

            # new date row
            if record[1] != old_date:
                
                if current_row >= old_rows:
                    self.grid.InsertRows(current_row)
                
                self.__populateDateRow(self.grid, current_row, record[1], enabled)
                old_date = record[1]
                current_row += 1

            if current_row >= old_rows:
                self.grid.InsertRows(current_row)

            self.__populateClearRow(self.grid, current_row, record[0], record[2], record[3], record[4], record[5], enabled)
        
            current_row += 1

        # trim any unused rows
        if current_row < old_rows:
            self.grid.DeleteRows(current_row, old_rows - current_row)


    def __populateDateRow(self, parent, row, date, enabled):

        """
        Populate a date row as a divider in the grid, reset the cell sizes in 
        case it was a clear row the last time round
        """

        parent.SetCellSize(row, 0, 1, 5)

        attr = wxgrid.GridCellAttr()

        if enabled:
            attr.SetBackgroundColour(DATE_ROW)
        else:
            attr.SetBackgroundColour(DATE_ROW_ALT)

        parent.SetRowAttr(row, attr)

        parent.SetCellValue(row, 0, 'Clears with a start date of : ' + date)


    def __populateClearRow(self, parent, row, WO, start, end, reason, summary, enabled):

        """
        Populate a clear row, reset the cell sizes in case it was a date row
        the last time round
        """

        parent.SetCellSize(row, 0, 1, 1)
        parent.SetCellSize(row, 1, 1, 1)
        parent.SetCellSize(row, 2, 1, 1)
        parent.SetCellSize(row, 3, 1, 1)
        parent.SetCellSize(row, 4, 1, 1)

        attr = wxgrid.GridCellAttr()

        if enabled:
            attr.SetBackgroundColour(CLEAR_ROW)
        else:
            attr.SetBackgroundColour(CLEAR_ROW_ALT)

        parent.SetRowAttr(row, attr)

        parent.SetCellValue(row, 0, WO)
        parent.SetCellValue(row, 1, start)
        parent.SetCellValue(row, 2, end)
        parent.SetCellValue(row, 3, ' ' + reason)
        parent.SetCellValue(row, 4, ' ' + summary.replace('\n', ' '))


    def modifyRecord(self, record):

        """
        Modify or add the 'record' in the dbase, update the grid
        """

        WO = record.data['WO']

        for i in range(len(self.dbase)):
            if self.dbase[i]['WO'] == WO:
                self.dbase[i] = record.data
                break

        else:
            self.dbase.append(record.data)

        self.__saveRecords()
        self.__updateGrid(self.grid)


    def deleteRecord(self, WO):

        """
        Delete the 'WO' record from the dbase, update the grid
        """

        for i in range(len(self.dbase)):
            if self.dbase[i]['WO'] == WO.data:
                del self.dbase[i]
                break

        self.__saveRecords()
        self.__updateGrid(self.grid)


    def __daemonGrid(self, event = ''):

        """
        Painful hack because its not possible to block selection of date rows
        in the grid. Note 'Timer' must be stopped on exiting class else zombie 
        process is created 
        """

        top_left  = [ x[0] for x in self.grid.GetSelectionBlockTopLeft()]
        bot_right = [ x[0] for x in self.grid.GetSelectionBlockBottomRight()]

        need_refresh = False

        for i in range(len(top_left)):
            for j in range(top_left[i], bot_right[i] + 1):

                WO = str(self.grid.GetCellValue(j, 0)).strip()

                # process date rows
                if len(WO) != 8:
                    self.grid.DeselectRow(j)
                    need_refresh = True

        if need_refresh:
            self.grid.Refresh()

# ------------------------------------------------------------------------------


    def __onWizard(self, event = ''):

        """ 
        Start the wizard 
        """

        self.__wizard()


    def __onAddNewClear(self, event = ''):

        """ Open the add new clear dialog """

        self.grid.ClearSelection()
        AddNewClear(self, self.fx_wizard_dir)

    
    def __onEditSelectedClear(self, event):

        """
        A wrapper around instantiating the 'EditExistingClear' class
        """

        self.grid.ClearSelection()
        row = event.GetRow()
        WO = str(self.grid.GetCellValue(row, 0)).strip()

        # skip any date rows that may have been selected in error
        if len(WO) != 8:
            self.grid.ClearSelection()

        else:
            for record in self.dbase:
                if record['WO'] == WO:
                    EditExistingClear(self, self.fx_wizard_dir, record)
                    break


    def __onDelSelectedClears(self, event = ''):

        """
        A tricky bit of code, wxPython is a bit of a mess here, you have to
        calculate the selected rows by looking at selected blocks
        """

        top_left  = [ x[0] for x in self.grid.GetSelectionBlockTopLeft()]
        bot_right = [ x[0] for x in self.grid.GetSelectionBlockBottomRight()]

        for i in range(len(top_left)):
            for j in range(top_left[i], bot_right[i] + 1):

                WO = str(self.grid.GetCellValue(j, 0)).strip()

                # skip any date rows that may have been selected in error
                if len(WO) != 8:
                    continue

                # has to be in reverse else multiple deletes corrupt index
                for ptr in range(len(self.dbase) - 1, -1, -1):
                    if self.dbase[ptr]['WO'] == WO:
                        del self.dbase[ptr]

        self.grid.ClearSelection()
        self.__saveRecords()
        self.__updateGrid(self.grid)


    def __onDelAllClears(self, event = ''):

        self.dbase = []
        self.__saveRecords()
        self.__updateGrid(self.grid)


    def __onAbout(self, event = ''):

        AboutWindow(self, self.fx_wizard_dir)


    def __onHelp(self, event = ''):

        HelpWindow(self, self.fx_wizard_dir)


    def __onBugDiagSave(self, event = ''):

        BugDiagSave(self, self.log.retrieveLog())


    def __onQuit(self, event = ''):

        self.timer.Stop()
        self.Destroy()


    def __onSize(self, event = ''):
        summary_size = self.grid.GetClientSize()[0] - (3 * 80 + 200)
        self.grid.SetColSize(4, summary_size)
        self.grid.SetScrollLineX(1) # hide horizontal scroll bar
        event.Skip() # propagate onSize event


    def __saveRecords(self):

        """ 
        Save the 'dbase' as a pickle 
        """

        f_obj = open(os.path.join(self.fx_wizard_dir, 'data', 'dbase.pik'), 'wb')
        pickle.dump(self.dbase, f_obj)
        f_obj.close()


    def __loadRecords(self):

        """ 
        Load the 'dbase' from a pickle 
        """

        f_obj = open(os.path.join(self.fx_wizard_dir, 'data', 'dbase.pik'), 'rb')
        self.dbase = pickle.load(f_obj)
        f_obj.close()


# ------------------------------------------------------------------------------


    def __wizard(self):

        """
        This has proved more difficult than at first thought. The status 
        window need to be instantiated first, then a thread for the main 
        processing started. The main processing '__wizardThread()' then starts
        instatiating mymobiler and communicates back to the rest of the script 
        via 'wx.CallAfter()' calls 

        If you try and run the GUI and the main processing from the same method
        you get GUI locks. The GUI code needs to be allowed to exit after any
        update to allow it to draw properly
        """

        # check for any reacords
        if len(self.dbase) == 0:
             wx.MessageBox('No entries in the database\nso nothing to clear.', 'Info', wx.OK | wx.ICON_INFORMATION)
             return

        # calculations for the positioning of the status window and the 
        # mymobiler window ...

        # expected size of MyMobiler window
        win_x = 246
        win_y = 404 

        # grab the raw screen dimensions
        img = ImageGrab.grab()
        screen_x, screen_y = img.size

        # 'status window' is 3 * 246 = 738, space between 'status window'
        # and 'MyMobiler window' 10, so total width = 738 + 10 + 246 = 994
        # 'status window' x = (screen_x / 2) - (994 / 2) which is the 
        # same as  x = (screen_x / 2) - 497 :)

        mystatus_x = screen_x / 2 - 497
        mystatus_y = screen_y / 2 - win_y / 2

        # 'MyMobiler window' x = (screen_x / 2) + (994 / 2) - 246 which is the 
        # same as  x = (screen_x / 2) + 251 :)

        mymobiler_x = screen_x / 2 + 251
        mymobiler_y = mystatus_y

        self.mystatus = StatusWindow(self, self.fx_wizard_dir, mystatus_x, mystatus_y)
        thread.start_new_thread(self.__wizardThread, (self.dbase, mymobiler_x, mymobiler_y))


    def __wizardThread(self, dbase, mymobiler_x, mymobiler_y):

        """
        The main wizard processing thread. It uses 'engine.py' to control 
        MyMobiler and 'wx.CallAfter()' calls to control the main script GUI 
        """

        # oh and another win32 gotcha, this is running in a thread, see ...
        # http://python.6.n6.nabble.com/Dispatch-error-CoInitialize-has-not-been-called-td1951088.html
        pythoncom.CoInitialize() 

        try:
            self.mymobiler = engine.Engine(mymobiler_x, mymobiler_y)

        except WizardExcept as err:
            err = str(err)

            if err == 'no_window':
                wx.MessageBox("'MyMobiler' is not running,\nstart 'MyMobiler' first", 'Info', wx.OK | wx.ICON_INFORMATION)

            elif err == 'black_window' or err == 'wrong_size': 
                wx.MessageBox("The PDA has not linked to MyMobiler,\ncheckout FX Wizard 'help'", 'Info', wx.OK | wx.ICON_INFORMATION)

            
            wx.CallAfter(self.mystatus.quit)
            self.mymobiler.minimize()
            return

        wx.CallAfter(self.__enableMainGUI, False)
        wx.CallAfter(self.mystatus.update, 100, 'Accepting Alerts')





        try:
            self.mymobiler.acceptAlerts()

        except WizardExcept as err:
            err = str(err)

            wx.CallAfter(self.mystatus.quit)
            self.mymobiler.minimize()

            if err == 'csr_moved':
                wx.MessageBox("'The mouse has been moved,\nClearing has been aborted", 'Info', wx.OK | wx.ICON_INFORMATION)

            wx.CallAfter(self.__enableMainGUI, True)
            return






        # # all wrapped in a try / except to capture and print any exceptions
        # try:

        #     Publisher().sendMessage('wizard_ctrl', ('status_update', 100, 'Accepting Alerts'))
        #     self.mymobiler.acceptAlerts()

        #     Publisher().sendMessage('wizard_ctrl', ('status_update', 90, 'Alerts Accepted'))


            
        #     # self.mymobiler.minimize()
        #     # Publisher().sendMessage('wizard_ctrl', ('status_quit'))


        # except:
        #     exc_type, exc_value, exc_traceback = sys.exc_info()
        #     exc_trace = traceback.extract_tb(exc_traceback)[-1]
        #     print '######################################################################'
        #     print sys.exc_info()
        #     print '######################################################################'
        #     tmp = traceback.extract_tb(exc_traceback)
        #     for i in tmp:
        #         print i
        #     print '######################################################################'
        #     print '%s' % exc_trace[0]
        #     print '%s(), Line %s, "%s"' % (exc_trace[2], exc_trace[1], exc_trace[3])























# ------------------------------------------------------------------------------

class AboutWindow(wx.Frame):

    """
    A license window, oh this software is GPL v3 by the way :), one ugly hack,
    could not change the background color in the 'HtmlWindow' so edited the
    HTML directly and inserted the colour 
    """

    def __init__(self, parent, fx_wizard_dir):
        wx.Frame.__init__(self, parent, title='About FX Wizard', size=(400, 357),
        style=wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.RESIZE_BOX | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))

        icon = wx.Icon(os.path.join(fx_wizard_dir, 'data', 'wizard.ico'), wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)

        html = wxhtml.HtmlWindow(self, style=wx.BORDER_SUNKEN)
        html.LoadPage(os.path.join(fx_wizard_dir, 'data', 'about.html'))

        self.MakeModal(True)
        self.Center(wx.BOTH)
        self.Show(True)

        self.Bind(wx.EVT_CLOSE, self.__onExit)

    def __onExit(self, event = ''):

        self.MakeModal(False)
        self.Destroy()


# ------------------------------------------------------------------------------


class BugDiagSave(wx.FileDialog):

    """ 
    A save diagnostic dialog ... 
    """

    def __init__(self, parent, file_):

        dlg = wx.FileDialog(parent, 'Save diagnostic file', '', 'FX Wizard.dbg', '*.dbg', style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)

        if dlg.ShowModal() == wx.ID_OK:
            dlg.Destroy()
            shutil.copyfile(file_, os.path.join(dlg.GetDirectory(), dlg.GetFilename()))

        else:
            dlg.Destroy()


# ------------------------------------------------------------------------------


class HelpWindow(wx.Frame):

    """ 
    A help window, one ugly hack, could not change the background color in the 
    'HtmlWindow' so edited the HTML directly and inserted the colour
    """

    def __init__(self, parent, fx_wizard_dir):

        wx.Frame.__init__(self, parent, title='Help using FX Wizard', size=(600, 480),
        style=wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.RESIZE_BOX | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))

        icon = wx.Icon(os.path.join(fx_wizard_dir, 'data', 'wizard.ico'), wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)

        html = wxhtml.HtmlWindow(self, style=wx.BORDER_SUNKEN)
        html.LoadPage(os.path.join(fx_wizard_dir, 'data', 'help.html'))

        self.MakeModal(True)
        self.Center(wx.BOTH)
        self.Show(True)

        self.Bind(wx.EVT_CLOSE, self.__onExit)

    def __onExit(self, event):
        self.MakeModal(False)
        self.Destroy()


# ------------------------------------------------------------------------------


class ClearLib(wx.Frame):

    def __init__(self, parent, fx_wizard_dir, frame_title):

        wx.Frame.__init__(self, parent, title = frame_title, size = (430, 683),
        style=wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.RESIZE_BOX | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))

        # needs a panel to allow TAB's to work
        self.panel = wx.Panel(self)

        icon = wx.Icon(os.path.join(fx_wizard_dir, 'data', 'wizard.ico'), wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)

        # so stop recursive loops
        self.processLink4inhibit = False

        # max allowed on site time as a 'HH:MM' string
        self.max_onsite = ''

        self.SetBackgroundColour(WINDOW_NORM)
        self.Center(wx.BOTH)

        self.__setupInstance()
        self.__setupGUI()
        self.CreateStatusBar()
        self.initForm()

        self.MakeModal(True)
        self.Show(True)


    def __setupInstance(self):

        """
        Setup instance variables, not strictly needed but good practice to
        define everything at the start
        """

        # defining instance variables and constants
        self.data1, self.data2, self.data3, self.data4    = 0, 0, 0, 0
        self.data5, self.data6, self.data7, self.data8    = 0, 0, 0, 0
        self.data9, self.data10, self.data11, self.data12 = 0, 0, 0, 0

        # colours as a workaround for the 'TimeCtrl' bug, used by 'self.__refresh'
        self.data3_colour = WINDOW_EMP
        self.data5_colour = WINDOW_EMP
        self.data6_colour = WINDOW_EMP

        # drop down reason code list
        self.codes = ['', '00 Burglary Equip Damaged', '01 Burglary Equip not Damaged',
        '02 Panic Button Operated by ...', '03 Line Fault', '04 Subscriber Error',
        '05 Storm/Electrical Transit', '06 Prolonged Power Failure',
        '07 Tamper', '08 Passive Infra Red Detector', '09 Dual PIR / Microwave Det ...',
        '10 Other Electronic Device', '11 Non Electronic Device',
        '12 Control/Signalling Equipment', '13 Repair Damage', '14 Change Customer Code',
        '15 Re-Instruct Customer', '16 Additional Work', '17 Completion Certificate',
        '18 Call Out no Access', '19 Routine Maint Completed', 
        '20 Routine Maint no Access', '21 Maint 2nd Written Attempt',
        '22 Equipment Fault', '37 Power Failure', '38 Telephone \ Assistance']

        # status of completed form sections
        self.WO_OK, self.dates_OK     = False, False
        self.site_OK, self.reason_OK  = False, False
        self.report_OK, self.price_OK = False, False


    def __setupGUI(self):

        """
        An overly long method to set up the form GUI. I looked at re-factoring
        this but there is no easy way to break it down further
        """

        # the times section
        text1  = wx.StaticText(self.panel, -1, 'Work Order :', size = (63, -1))
        stext1 = wx.BoxSizer(wx.VERTICAL)
        stext1.Add(text1, 0, wx.TOP, 3)
        self.data1  = wx.TextCtrl(self.panel, -1, '', size = (185, -1))

        text2  = wx.StaticText(self.panel, -1, 'Start Date :')
        stext2 = wx.BoxSizer(wx.VERTICAL)
        stext2.Add(text2, 0, wx.TOP, 3)
        self.data2  = wx.DatePickerCtrl(self.panel, -1, size = (185, -1), style = wx.DP_DROPDOWN)

        text3  = wx.StaticText(self.panel, -1, 'Start Time : ')
        stext3 = wx.BoxSizer(wx.VERTICAL)
        stext3.Add(text3, 0, wx.TOP, 3)
        self.data3  = wxmasked.TimeCtrl(self.panel, -1, '00:00:00', format = '24HHMM')

        text4  = wx.StaticText(self.panel, -1, 'End Date :')
        stext4 = wx.BoxSizer(wx.VERTICAL)
        stext4.Add(text4, 0, wx.TOP, 3)
        self.data4  = wx.DatePickerCtrl(self.panel, -1, size = (185, -1), style = wx.DP_DROPDOWN)

        text5  = wx.StaticText(self.panel, -1, 'End Time :')
        stext5 = wx.BoxSizer(wx.VERTICAL)
        stext5.Add(text5, 0, wx.TOP, 3)
        self.data5  = wxmasked.TimeCtrl(self.panel, -1, '00:00:00', format = '24HHMM')

        text6  = wx.StaticText(self.panel, -1, 'On Site :')
        stext6 = wx.BoxSizer(wx.VERTICAL)
        stext6.Add(text6, 0, wx.TOP, 3)
        self.data6  = wxmasked.TimeCtrl(self.panel, -1, '00:00:00', format = '24HHMM')

        sizer1 = wx.FlexGridSizer(cols = 4, hgap = 10, vgap = 6)
        sizer1.AddMany([ stext1, self.data1, (0, 0), (0, 0),
                        stext2, self.data2, stext3, self.data3,
                        stext4, self.data4, stext5, self.data5,
                        (0, 0), (0, 0), stext6, self.data6])

        # the report section
        text7  = wx.StaticText(self.panel, -1, 'Reason :', size = (63, -1))
        stext7 = wx.BoxSizer(wx.VERTICAL)
        stext7.Add(text7, 0, wx.TOP, 3)

        self.data7 = wx.ComboBox(self.panel, -1, '', size = (185, -1), 
        choices = self.codes, style = wx.CB_DROPDOWN)

        text8  = wx.StaticText(self.panel, -1, 'Report :')
        stext8 = wx.BoxSizer(wx.VERTICAL)
        stext8.Add(text8, 0, wx.TOP, 3)
        self.data8  = wx.TextCtrl(self.panel, -1, size = (185, 150), style = wx.TE_MULTILINE)
        self.data8.SetInsertionPoint(0)

        sizer2 = wx.FlexGridSizer(cols = 4, hgap = 10, vgap = 6)
        sizer2.AddMany([stext7, self.data7, (0, 0), (0, 0),
                        stext8, self.data8])

        # the misc section
        text9  = wx.StaticText(self.panel, -1, 'Fixed \xa3 :', size = (63, -1))
        stext9 = wx.BoxSizer(wx.VERTICAL)
        stext9.Add(text9, 0, wx.TOP, 3)
        self.data9  = wx.TextCtrl(self.panel, -1, '', size = (185, -1))

        text10  = wx.StaticText(self.panel, -1, 'Misc 1 :')
        stext10 = wx.BoxSizer(wx.VERTICAL)
        stext10.Add(text10, 0, wx.TOP, 3)
        self.data10  = wx.ComboBox(self.panel, -1, 'In Hours', size = (185, -1), 
        choices = ['In Hours', 'Out of Hours'], style = wx.CB_DROPDOWN | wx.CB_READONLY)

        text11  = wx.StaticText(self.panel, -1, 'Misc 2 :')
        stext11 = wx.BoxSizer(wx.VERTICAL)
        stext11.Add(text11, 0, wx.TOP, 3)
        self.data11  = wx.ComboBox(self.panel, -1, 'Attendance Chargeable', size = (185, -1), 
        choices = ['Attendance Chargeable', 'Attendance not Chargeable'], 
        style = wx.CB_DROPDOWN | wx.CB_READONLY)

        text12  = wx.StaticText(self.panel, -1, 'Misc 3 :')
        stext12 = wx.BoxSizer(wx.VERTICAL)
        stext12.Add(text12, 0, wx.TOP, 3)
        self.data12  = wx.ComboBox(self.panel, -1, 'Additional Docs Required', size = (185, -1), 
        choices = ['Additional Docs Required', 'No Additional Docs Required'], 
        style = wx.CB_DROPDOWN | wx.CB_READONLY)


        sizer3 = wx.FlexGridSizer(cols = 4, hgap = 10, vgap = 6)
        sizer3.AddMany([stext9, self.data9, (0, 0), (0, 0),
                        stext10, self.data10, (0, 0), (0, 0),
                        stext11, self.data11, (0, 0), (0, 0),
                        stext12, self.data12, (0, 0), (0, 0)])

        # the parts section
        text, data = [], []
        for i in range(1, 5):

            ntext  = wx.StaticText(self.panel, -1, 'Parts %i :' % i, size = (63, -1))
            ntext.Enable(False)
            nstext = wx.BoxSizer(wx.VERTICAL)
            nstext.Add(ntext, 0, wx.TOP, 3)
            text.append(nstext)

            ndata  = wx.ComboBox(self.panel, -1, '7AH Battery', size = (185, -1), 
            choices = ['7AH Battery'], style = wx.CB_DROPDOWN)
            ndata.Enable(False)
            data.append(ndata)

            ntext  = wx.StaticText(self.panel, -1, 'Quantity :')
            ntext.Enable(False)
            nstext = wx.BoxSizer(wx.VERTICAL)
            nstext.Add(ntext, 0, wx.TOP, 3)
            text.append(nstext)

            ndata  = wx.ComboBox(self.panel, -1, '0', size = (43, -1), 
            choices = ['0'], style = wx.CB_DROPDOWN)
            ndata.Enable(False)
            data.append(ndata)

        sizer4 = wx.FlexGridSizer(cols = 4, hgap = 10, vgap = 6)
        sizer4.AddMany([text[0], data[0], text[1], data[1],
                        text[2], data[2], text[3], data[3],
                        text[4], data[4], text[5], data[5],
                        text[6], data[6], text[7], data[7]])

        # the buttons
        sizer5 = self.ctrlButtons()

        # generates nice padding around the grid
        border = wx.BoxSizer(wx.VERTICAL)
        border.Add(sizer1, 0, wx.TOP | wx.LEFT | wx.RIGHT, 20)
        border.Add(wx.StaticLine(self.panel), 0, wx.ALL | wx.EXPAND, 10)
        border.Add(sizer2, 0, wx.LEFT | wx.RIGHT, 20)
        border.Add(wx.StaticLine(self.panel), 0, wx.ALL | wx.EXPAND, 10)
        border.Add(sizer3, 0, wx.LEFT | wx.RIGHT, 20)
        border.Add(wx.StaticLine(self.panel), 0, wx.ALL | wx.EXPAND, 10)
        border.Add(sizer4, 0, wx.LEFT | wx.RIGHT, 20)
        border.Add(wx.StaticLine(self.panel), 0, wx.ALL | wx.EXPAND, 10)
        border.Add(sizer5, 0, wx.CENTER, 20)
        self.panel.SetSizer(border)

        # the tab sequence
        self.tabSequence()

        # all the bindings
        self.Bind(wx.EVT_CLOSE, self.quitButton)
        self.Bind(wx.EVT_TEXT, self.processLink1, self.data1)
        self.Bind(wx.EVT_DATE_CHANGED, self.preProcess1stDate, self.data2)
        self.Bind(wx.EVT_TEXT, self.processLink2, self.data3)
        self.Bind(wx.EVT_DATE_CHANGED, self.processLink2, self.data4)
        self.Bind(wx.EVT_TEXT, self.processLink2, self.data5)
        self.Bind(wx.EVT_TEXT, self.processLink3, self.data6)
        self.Bind(wx.EVT_TEXT, self.processLink4, self.data7)
        self.Bind(wx.EVT_COMBOBOX, self.processLink4, self.data7)
        self.Bind(wx.EVT_TEXT, self.processLink5, self.data8)
        self.Bind(wx.EVT_TEXT, self.processLink6, self.data9)

        # nasty hack !
        self.timer = wx.Timer(self)
        self.timer.Start(200)
        self.Bind(wx.EVT_TIMER, self.__daemonForm, self.timer)
        self.ctrlButtonsDisabled()


    def ctrlButtons(self):

        """
        To be super classed, needs to return a sizer, normally a 'BoxSizer'
        containing the buttons
        """

        pass

    def ctrlButtonsEnabled(self):

        """
        To be super classed, 
        """

        pass


    def ctrlButtonsDisabled(self):

        """
        To be super classed, 
        """

        pass


    def tabSequence(self):

        """
        To be super classed, generally uses something like :
        """

        # # tweak the TAB order for speed of data entry
        # tab_order = (self.data1, self.data3, self.data5, self.data7, 
        # self.data8, self.save_btn, self.abort_btn)

        # for i in range(len(tab_order) - 1):
        #     tab_order[i + 1].MoveAfterInTabOrder(tab_order[i])

        pass


    def initForm(self):

        """
        To be super classed, needs to call 'processLink1' when completed
        """

        pass


    def wipeRecord(self):

        """
        Reset all values including the work order to their original state
        """
    
        self.wipeButton()
        self.data1.SetValue('')
        # set focus because wipe sets it to 'data3'
        self.data1.SetFocus()


    def wipeButton(self, event = ''):

        """
        Reset all values except the work order to their original state
        """

        today = wx.DateTime()
        today.SetToCurrent()
        today.GetDateOnly()

        self.data2.SetValue(today)
        self.data3.SetValue('00:00')
        self.data4.SetValue(today)
        self.data5.SetValue('00:00')
        self.data6.SetValue('00:00')

        self.data7.SetStringSelection(self.codes[0])
        self.data8.Clear()
        self.data9.Clear()

        self.data10.SetStringSelection('In Hours')
        self.data11.SetStringSelection('Attendance Chargeable')
        self.data12.SetStringSelection('Additional Docs Required')

        self.data3.SetBackgroundColour(WINDOW_EMP)
        self.data5.SetBackgroundColour(WINDOW_EMP)
        self.data7.SetBackgroundColour(WINDOW_EMP)
        self.data8.SetBackgroundColour(WINDOW_EMP)

        self.data3.SetFocus()
        self.processLink1()


    def quitButton(self, event = ''):

        """
        Quit, oh and kills the daemon process
        """

        self.timer.Stop()
        self.MakeModal(False)
        self.Destroy()


    def saveRecord(self):

        """
        Generate a database record from the data in the form, make a dictionary 
        with friendly keys and send it via 'Publisher' to the main GUI
        """

        WO = str(self.data1.GetValue())

        start_date = str(self.data2.GetValue())[:8]
        start_time = str(self.data3.GetValue())
        end_date   = str(self.data4.GetValue())[:8]
        end_time   = str(self.data5.GetValue())
        on_site    = str(self.data6.GetValue())

        reason = str(self.data7.GetValue())
        report = self.data8.GetValue().encode("ascii", "ignore").strip()
        price  = str(self.data9.GetValue())

        misc1 = str(self.data10.GetValue())
        misc2 = str(self.data11.GetValue())
        misc3 = str(self.data12.GetValue())

        record = {'WO' : WO, 'start_date': start_date, 
        'start_time': start_time, 'end_date': end_date, 'end_time': end_time, 
        'on_site': on_site, 'reason': reason, 'report': report, 'price': price,
        'misc1': misc1, 'misc2': misc2, 'misc3': misc3}

        Publisher().sendMessage('modify_record', record)


    def deleteRecord(self):

        """
        Remove the database record displayed in the form, send it via 
        'Publisher' to the main GUI
        """

        Publisher().sendMessage('delete_record', str(self.data1.GetValue()))


    def populateForm(self, record):

        """
        Populate form with data from a database record. If work order field is 
        blank then wipe the form
        """

        # if no work order, wipe everything clean
        if record['WO'] == '':

            self.wipeRecord()

        else:

            dt = wx.DateTime()

            self.data1.SetValue(record['WO'])

            req_date = record['start_date'].split('/')
            dt.Set(int(req_date[1]), int(req_date[0]) - 1, int('20' + req_date[2]))
            self.data2.SetValue(dt)

            self.data3.SetValue(record['start_time'])
            req_date = record['end_date'].split('/')
            dt.Set(int(req_date[1]), int(req_date[0]) - 1, int('20' + req_date[2]))
            self.data4.SetValue(dt)

            self.data5.SetValue(record['end_time'])

            self.data6.SetValue(record['on_site'])

            self.data7.SetStringSelection(record['reason'])
            self.data8.Clear()
            self.data8.SetValue(record['report'])
            self.data9.Clear()
            self.data9.SetValue(record['price'])

            self.data10.SetStringSelection(record['misc1'])
            self.data11.SetStringSelection(record['misc2'])
            self.data12.SetStringSelection(record['misc3'])


    def __daemonForm(self, event = ''):

        """
        Painful hack because 'TimeCtrl' resets its background colour when its 
        tabbed over. Note 'Timer' must be stopped on exiting class else zombie 
        process is created
        """

        need_refresh = False

        if self.data3.GetBackgroundColour() != self.data3_colour:
            self.data3.SetBackgroundColour(self.data3_colour)
            need_refresh = True

        if self.data5.GetBackgroundColour() != self.data5_colour:
            self.data5.SetBackgroundColour(self.data5_colour)
            need_refresh = True

        if self.data6.GetBackgroundColour() != self.data6_colour:
            self.data6.SetBackgroundColour(self.data6_colour)
            need_refresh = True

        if need_refresh:
            self.Refresh()


    def preProcess1stDate(self, event = ''):

        """
        Set the end date to equal the start date if the start date has been 
        changed
        """

        start_date  = self.data2.GetValue()
        self.data4.SetValue(start_date)
        self.processLink2


    def processLink1(self, event = ''):

        """
        Process link 1 of the chain. Pretty straight forward, checks the work
        order 
        """

        # work order 
        if not re.match('^\d{8}$', self.data1.GetValue()):
            self.SetStatusText('Work order number, needs 8 numerical digits')
            self.data1.SetBackgroundColour(WINDOW_EMP)
            self.WO_OK = False
            self.ctrlButtonsDisabled()

        else:
            self.data1.SetBackgroundColour(WHITE)
            self.WO_OK = True
            self.processLink2()

        self.Refresh()


    def processLink2(self, event = ''):

        """
        Process link 2 of the chain. A whole lot of string splicing and 
        calculating to verify times and dates, sets the In/Out of hours
        'ComboBox' and sets the color variable for '__daemonForm'
        """

        # dates and times
        date  = str(self.data2.GetValue())[:8].split('/')
        time  = self.data3.GetValue().split(':')
        start = dt.datetime(int('20' + date[2]), int(date[0]), 
        int(date[1]), int(time[0]), int(time[1]))

        date  = str(self.data4.GetValue())[:8].split('/')
        time  = self.data5.GetValue().split(':')
        end   = dt.datetime(int('20' + date[2]), int(date[0]), 
        int(date[1]), int(time[0]), int(time[1]))

        # set the 'In Hours', 'Out of Hours' drop down
        if int(start.strftime('%w')) in [0, 6] or 16 > int(start.strftime('%H')) < 9:
            self.data10.SetStringSelection('Out of Hours')
        else:
            self.data10.SetStringSelection('In Hours')

        self.dates_OK = False
        # check for start future time
        if start > start.now():
            self.SetStatusText('Start Date / Time is in the future')
            self.data6.SetValue('00:00')
            self.data3_colour = WINDOW_EMP
            self.data5_colour = WHITE
            self.ctrlButtonsDisabled()

        # check for end future time
        elif end > start.now():
            self.SetStatusText('End Date / Time is in the future')
            self.data6.SetValue('00:00')
            self.data3_colour = WHITE
            self.data5_colour = WINDOW_EMP
            self.ctrlButtonsDisabled()

        # check for reverse time
        elif start > end:
            self.SetStatusText('Start Date / Time is later than end Date / Time')
            self.data6.SetValue('00:00')
            self.data3_colour = WINDOW_EMP
            self.data5_colour = WINDOW_EMP
            self.ctrlButtonsDisabled()

        # check for same time
        elif start == end:
            self.SetStatusText('Start Date / Time is equal to end Date / Time')
            self.data6.SetValue('00:00')
            self.data3_colour = WINDOW_EMP
            self.data5_colour = WINDOW_EMP
            self.ctrlButtonsDisabled()

        # check for 24 hrs
        elif end - start >= dt.timedelta(1):
            self.SetStatusText('On site time is greater than 24 hrs')
            self.data6.SetValue('23:59')
            self.data3_colour = WINDOW_EMP
            self.data5_colour = WINDOW_EMP
            self.ctrlButtonsDisabled()

        else:
            diff_mins = int((end - start).total_seconds() / 60)
            diff_str  = '%02i:%02i' % (diff_mins / 60, diff_mins % 60)
            self.max_onsite = diff_str
            self.data6.SetValue(diff_str)
            self.data3_colour = WHITE
            self.data5_colour = WHITE
            self.dates_OK = True
            self.processLink3()

        self.Refresh()


    def processLink3(self, event = ''):

        """ 
        Process link 3 of the chain. Checking to see if on site time is within
        bounds and sets the color variable for '__daemonForm'
        """

        raw_time = self.data6.GetValue().split(':')
        fmt_time = '%02i:%02i' % (int(raw_time[0]), int(raw_time[1]))
        self.site_OK = False

        if self.max_onsite == '':
            self.SetStatusText('Enter Start Date / Time and end Date / Time first')
            self.data6_colour = WINDOW_EMP

        elif fmt_time == '00:00':
            self.SetStatusText("On Site time can not be '00:00'")
            self.data6_colour = WINDOW_EMP

        elif fmt_time > self.max_onsite:
            self.SetStatusText('On Site time too long, maximum of ' + self.max_onsite)
            self.data6_colour = WINDOW_EMP

        else:
            self.data6_colour = WHITE
            self.site_OK = True
            self.processLink4()


    def processLink4(self, event = ''):

        """
        Process link 4 of the chain. I am sure there is a better way to code 
        this just haven't worked out how yet :), selectively fills in the reason
        text box if it makes sense
        """

        # avoids recursion overload, 'WriteText' generates its own events !
        if not self.processLink4inhibit:

            self.processLink4inhibit = True
            text = self.data7.GetValue()
            if len(text) == 2:

                try:
                    i = int(text)

                except ValueError:
                    i = -1
                
                if -1 < i < 23:
                    pass

                elif i == 37 or i == 38:
                    i -= 14

                else:
                    i = -1

                self.data7.SetStringSelection(self.codes[i + 1])
                if i in [3, 6, 14, 18, 19, 20, 38] and self.data8.GetValue().strip() == '':
                    self.data8.WriteText(self.codes[i + 1][2:])

            if len(self.data7.GetValue()) > 2:

                i = int(self.data7.GetValue()[:2])
                if i in [3, 6, 14, 18, 19, 20, 38] and self.data8.GetValue().strip() == '':
                    self.data8.WriteText(self.codes[i + 1][2:])

                self.data7.SetBackgroundColour(WHITE)
                self.reason_OK = True
                self.processLink4inhibit = False
                self.processLink5()

            else:
                self.SetStatusText('Select a reason code')
                self.data7.SetBackgroundColour(WINDOW_EMP)
                self.ctrlButtonsDisabled()
                self.reason_OK = False
                self.processLink4inhibit = False

            self.Refresh()


    def processLink5(self, event = ''):

        """
        Link 5 of the chain. Text entered here is not sanitized, that happens 
        when the form is submitted
        """

        # reason text
        report = self.data8.GetValue()

        if report.strip() == '':
            self.SetStatusText('Enter report text')
            self.data8.SetBackgroundColour(WINDOW_EMP)
            self.report_OK = False
            self.ctrlButtonsDisabled()

        else:
            self.SetStatusText('')
            self.data8.SetBackgroundColour(WHITE)
            self.report_OK = True
            self.processLink6()

        self.Refresh()


    def processLink6(self, event = ''):

        """
        Link 6 of the chain. Finally checking the fixed price. The process chain 
        drops out here if the minimum amount of data to perform a valid clear 
        has been entered
        """

        price_str = self.data9.GetValue()
        if price_str != '':

            # enforcing format and value limit
            if not re.match('^\d+\.\d{2}', price_str) or float(price_str) > 999.99:

                self.SetStatusText('Enter valid fixed price, format eg \xa3130.00 for \xa3130 and less than \xa31000')
                self.data9.SetBackgroundColour(WINDOW_EMP)
                self.price_OK = False
                self.ctrlButtonsDisabled()
                # messy but efficient :)
                return

        self.data9.SetBackgroundColour(WHITE)
        self.price_OK = True

        if self.WO_OK and self.dates_OK and self.site_OK and self.reason_OK and self.report_OK and self.price_OK:
            self.SetStatusText('')
            self.ctrlButtonsEnabled()

        else:
            self.processLink1()


# ------------------------------------------------------------------------------

            
class EditExistingClear(ClearLib):

    def __init__(self, parent, fx_wizard_dir, record):

        self.record = record
        ClearLib.__init__(self, parent, fx_wizard_dir, 'Edit Existing Clear')


    def initForm(self):
        
        """
        Superclass to setup a populated form,
        """

        self.populateForm(self.record)
        self.processLink1()


    def ctrlButtons(self):

        """
        Superclass to define control buttons at the bottom of the frame
        """

        # finally the buttons
        self.quit_btn = wx.Button(self.panel, -1, 'Quit', size = (90, -1))
        self.wipe_btn = wx.Button(self.panel, -1, 'Wipe', size = (90, -1))
        self.del_btn  = wx.Button(self.panel, -1, 'Delete', size = (90, -1))
        self.save_btn = wx.Button(self.panel, -1, 'Save', size = (90, -1))

        sizer5 = wx.BoxSizer(wx.HORIZONTAL)
        sizer5.Add(self.quit_btn, 0, wx.RIGHT, 5)
        sizer5.Add(self.wipe_btn, 0, wx.LEFT | wx.RIGHT, 5)
        sizer5.Add(self.del_btn,  0, wx.LEFT | wx.RIGHT, 5)
        sizer5.Add(self.save_btn, 0, wx.LEFT, 5)

        self.Bind(wx.EVT_BUTTON, self.quitButton, self.quit_btn)
        self.Bind(wx.EVT_BUTTON, self.wipeButton, self.wipe_btn)
        self.Bind(wx.EVT_BUTTON, self.delButton,  self.del_btn)
        self.Bind(wx.EVT_BUTTON, self.saveButton, self.save_btn)

        return sizer5


    def ctrlButtonsEnabled(self):

        self.save_btn.Enable(True)


    def ctrlButtonsDisabled(self):

        self.save_btn.Enable(False)


    def tabSequence(self):

        """
        Superclass to define tab sequencing 
        """

        # tweak the TAB order for speed of data entry
        tab_order = (self.data1, self.data3, self.data5, self.data7, self.data8,
        self.save_btn, self.quit_btn)

        for i in range(len(tab_order) - 1):
            tab_order[i + 1].MoveAfterInTabOrder(tab_order[i])


    def delButton(self, event = ''):

        """
        Delete really means delete then quit
        """

        self.deleteRecord()
        self.quitButton()


    def saveButton(self, event = ''):

        """
        Save really means save then quit
        """

        self.saveRecord()
        self.quitButton()


# ------------------------------------------------------------------------------


class AddNewClear(ClearLib):

    def __init__(self, parent, fx_wizard_dir):

        ClearLib.__init__(self, parent, fx_wizard_dir, 'Add New Clear')


    def initForm(self):
        
        """
        Superclass to setup an empty form, note work order empty triggers
        triggers an empty form
        """

        self.populateForm({'WO': ''})
        self.processLink1()


    def ctrlButtons(self):

        """
        Superclass to define control buttons at the bottom of the frame
        """

        # finally the buttons
        self.quit_btn     = wx.Button(self.panel, -1, 'Quit',  size = (90, -1))
        self.wipe_btn     = wx.Button(self.panel, -1, 'Wipe',  size = (90, -1))
        self.add_btn      = wx.Button(self.panel, -1, 'Add',   size = (90, -1))
        self.add_plus_btn = wx.Button(self.panel, -1, 'Add +', size = (90, -1))

        sizer5 = wx.BoxSizer(wx.HORIZONTAL)
        sizer5.Add(self.quit_btn, 0, wx.RIGHT, 5)
        sizer5.Add(self.wipe_btn, 0, wx.LEFT | wx.RIGHT, 5)
        sizer5.Add(self.add_btn,  0, wx.LEFT | wx.RIGHT, 5)
        sizer5.Add(self.add_plus_btn, 0, wx.LEFT, 5)

        self.Bind(wx.EVT_BUTTON, self.quitButton, self.quit_btn)
        self.Bind(wx.EVT_BUTTON, self.wipeButton, self.wipe_btn)
        self.Bind(wx.EVT_BUTTON, self.addButton, self.add_btn)
        self.Bind(wx.EVT_BUTTON, self.addPlusButton, self.add_plus_btn)

        return sizer5


    def ctrlButtonsEnabled(self):

        self.add_btn.Enable(True)
        self.add_plus_btn.Enable(True)


    def ctrlButtonsDisabled(self):

        self.add_btn.Enable(False)
        self.add_plus_btn.Enable(False)


    def tabSequence(self):

        """
        Superclass to define tab sequencing 
        """

        # tweak the TAB order for speed of data entry
        tab_order = (self.data1, self.data3, self.data5, self.data7, self.data8,
        self.add_plus_btn, self.add_btn, self.quit_btn)

        for i in range(len(tab_order) - 1):
            tab_order[i + 1].MoveAfterInTabOrder(tab_order[i])


    def addButton(self, event = ''):

        """
        Add really means save then quit 
        """

        self.saveRecord()
        self.quitButton()


    def addPlusButton(self, event = ''):

        """ 
        Add plus is save, wipe and do not quit 
        """

        self.saveRecord()
        end_date = self.data4.GetValue()
        self.wipeRecord()
        self.data2.SetValue(end_date)
        self.data4.SetValue(end_date)


# ------------------------------------------------------------------------------


class StatusWindow(wx.Frame):

    def __init__(self, parent, fx_wizard_dir, pos_x, pos_y):
        wx.Frame.__init__(self, parent, title='Status', pos=(pos_x, pos_y), size=(736, 404),
        style=wx.DEFAULT_FRAME_STYLE & ~ (wx.RESIZE_BORDER | wx.RESIZE_BOX | wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX))

        icon = wx.Icon(os.path.join(fx_wizard_dir, 'data', 'wizard.ico'), wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)
        self.SetBackgroundColour(WINDOW_NORM)

        self.time = self.status = 0
        self.daemon_flag = False

        self.__setupGUI()

        # make the clock blink
        self.timer = wx.Timer(self)
        self.timer.Start(700)
        self.Bind(wx.EVT_TIMER, self.__daemonStatus, self.timer)


    def __setupGUI(self):

        """
        Set up the status window GUI. Its amazing how long this took, I really
        struggle with box sizers.
        """

        self.panel = wx.Panel(self)

        font_msg  = wx.Font(20, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)
        font_time = wx.Font(66, wx.FONTFAMILY_MODERN, wx.NORMAL, wx.BOLD)
        font_warn = wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL)

        vsizer1 = wx.BoxSizer(wx.VERTICAL)

        text1 = wx.StaticText(self.panel, -1, 'Estimated Completion Time')
        text1.SetFont(font_msg)
        text1.SetForegroundColour(STATUS_NORM)
        vsizer1.Add(text1, 0, wx.TOP | wx.ALIGN_CENTER, 20)

        vsizer1.Add(wx.StaticLine(self.panel, style = wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 25)

        self.time = wx.StaticText(self.panel, -1, '--:--')
        self.time.SetFont(font_time)
        self.time.SetForegroundColour(STATUS_NORM)
        vsizer1.Add(self.time, 0, wx.ALIGN_CENTER, 0)

        vsizer1.Add(wx.StaticLine(self.panel, style = wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 25)

        self.status = wx.StaticText(self.panel, -1, 'Current Status : Idle')
        self.status.SetFont(font_msg)
        self.status.SetForegroundColour(STATUS_NORM)
        vsizer1.Add(self.status, 0, wx.ALIGN_CENTER, 0)

        vsizer1.Add(wx.StaticLine(self.panel, style = wx.LI_HORIZONTAL), 0, wx.TOP | wx.LEFT | wx.RIGHT | wx.EXPAND, 25)

        text2 = wx.StaticText(self.panel, -1, '* MOVE MOUSE TO ABORT CLEARING *')
        text2.SetFont(font_warn)
        text2.SetForegroundColour(STATUS_EMP)
        vsizer1.Add(text2, 0, wx.TOP | wx.ALIGN_CENTER, 12)

        self.panel.SetSizer(vsizer1)
        self.Show(True)


    def update(self, mins, status):

        """
        Update the estimated time and current status in the status window
        """

        mins = dt.timedelta(0, 0, 0, 0, mins)
        now  = dt.datetime.today()
        est  = now + mins

        self.time.SetLabel(est.strftime('%H:%M'))
        self.status.SetLabel('Current Status : ' + status)

        self.panel.Layout()


    def quit(self):

        """
        Kill the daemon then the GUI
        """

        self.timer.Stop()
        self.Destroy()


    def __daemonStatus(self, event = ''):

        """
        A simple daemon to flash the time on the status window. Note 'Timer' 
        must be stopped on exiting class else zombie process is created 
        """

        if self.daemon_flag:
            self.time.SetForegroundColour(STATUS_NORM)

        else:
            self.time.SetForegroundColour(STATUS_ALT)

        self.daemon_flag = not self.daemon_flag
        self.panel.Refresh()


















